#include "utils/fs_watcher.hpp"
#include "utils/log.hpp"

#if _WIN32
#include <Windows.h>
#include <utils/win32_utility.hpp>

#include <list>
#elif __unix__
#include <poll.h>
#include <fcntl.h>
#include <sys/inotify.h>
#include <cerrno>

#include <map>
#else
#error "Unsupported!"
#endif


#include <thread>
#include <atomic>

class FileSystemWatcher::FileSystemWatcherImpl
{
public:
    FileSystemWatcherImpl() {
        PlatformInit();
    }
    ~FileSystemWatcherImpl() {
        PlatformCleanup();
    }

    bool AddWatchDir(const char *dir)
    {
        if (watching_.load())
        {
            L_INFO("Already watching, could not add: %s", dir);
            return false;
        }
        return PlatformAddWatchDir(dir);
    }

    void StartWatch()
    {
        if (watching_.load())
            return;
        watching_.store (true);
        watch_thr_ = std::thread(&FileSystemWatcherImpl::PlatformWatchProc, this);
    }

    void StopWatch()
    {
        if (!watching_.load())
            return;
        watching_ = false;
        if (watch_thr_.joinable ())
            watch_thr_.join ();
    }

    void SetCallback(FileSystemChangedCallback cb)
    {
        callback_ = cb;
    }

private:
    void InvokeCallback(FileSystemChangedReason reason, const std::string &new_filename, const std::string &old_filename = {}) const
    {
        const char *files[] = {new_filename.c_str(), old_filename.c_str()};
        if (callback_.callback) {
            callback_.callback(reason, files, callback_.user);
        }
    }

    FileSystemChangedCallback callback_{nullptr, nullptr};

    // status control
    std::atomic<bool> watching_;
    std::thread       watch_thr_;

    // OS specific data & funcs
#if _WIN32
    struct WinWatcherData {
        u8          readBuffer[16384];
        HANDLE      hDirectory;
        OVERLAPPED  overlapped;
        std::string dir;
        BOOL        bQueued;
    };

    void PlatformInit()
    {

    }

    bool PlatformAddWatchDir(const char *path)
    {
        auto wDir = fromUtf8 (path);
        HANDLE hDir = CreateFileW(
            wDir.c_str (),
            FILE_LIST_DIRECTORY,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            nullptr,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
            nullptr
            );
        if (hDir != INVALID_HANDLE_VALUE)
        {
            auto data = new WinWatcherData;
            ZeroMemory(data, sizeof(WinWatcherData));
            data->hDirectory = hDir;
            ZeroMemory(&data->overlapped, sizeof(OVERLAPPED));
            data->overlapped.hEvent = CreateEventW(NULL, FALSE, FALSE, NULL);
            data->dir = path;
            watchers_.emplace_back(data);
            return true;
        }
        return false;
    }

    static void CALLBACK ReadCompletionRoutine(DWORD err, DWORD bytesTransfered, LPOVERLAPPED overlapped)
    {
        L_INFO("%d %d", err, bytesTransfered);
        int a = 0;
    }

    void PlatformWatchProc()
    {
        L_INFO("Watch started");
        while(watching_.load())
        {
            // queue async io
            for (auto const &watcher : watchers_)
            {
                if (watcher->bQueued)
                {
                    // try wait
                    DWORD code = WaitForSingleObject (watcher->overlapped.hEvent, 100);
                    if (code == WAIT_OBJECT_0)
                    {
                        // have result
                        DWORD bytes = 0;
                        BOOL ok = GetOverlappedResult (watcher->hDirectory, &watcher->overlapped, &bytes, FALSE);
                        if (!ok)
                        {
                            L_ERROR ("Get Result %d", GetLastError ());
                        }
                        else
                        {
                            u8 *ptr = watcher->readBuffer;
                            const FILE_NOTIFY_INFORMATION *info = (FILE_NOTIFY_INFORMATION*)ptr;

                            bool finished = false;
                            std::string paths[2];
                            int path_idx = 0;
                            FileSystemChangedReason reason;

                            do
                            {
                                std::string name = toUtf8 (std::wstring{info->FileName, info->FileName + info->FileNameLength/2});
                                paths[path_idx++] = watcher->dir + '\\' + name;
                                switch (info->Action)
                                {
                                case FILE_ACTION_ADDED:    reason = FileSystemChangedReason::eCreated;  finished = true; break;
                                case FILE_ACTION_REMOVED:  reason = FileSystemChangedReason::eDeleted;  finished = true; break;
                                case FILE_ACTION_MODIFIED: reason = FileSystemChangedReason::eModified; finished = true; break;
                                case FILE_ACTION_RENAMED_OLD_NAME: reason = FileSystemChangedReason::eRenamed; break;
                                case FILE_ACTION_RENAMED_NEW_NAME: finished = true; break;
                                default: break;
                                }

                                if (finished) {
                                    InvokeCallback(reason, paths[0], paths[1]);
                                    finished = false;
                                    path_idx = 0;
                                }

                                if (info->NextEntryOffset != 0)
                                {
                                    info = (FILE_NOTIFY_INFORMATION*)(ptr + info->NextEntryOffset);
                                }
                                else
                                {
                                    break;
                                }
                            } while(true);
                        }

                        // reset status
                        watcher->bQueued = FALSE;
                    }
                }
                else
                {
                    BOOL code = ReadDirectoryChangesW(
                        watcher->hDirectory,
                        watcher->readBuffer,
                        sizeof(WinWatcherData::readBuffer),
                        TRUE,
                        FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE,
                        NULL,
                        &watcher->overlapped,
                        NULL);
                    if (code)
                    {
                        watcher->bQueued = TRUE;
                    }
                    else
                    {
                        L_INFO("Queue Error %d", GetLastError ());
                    }
                }
            }
        }
        L_INFO("Watch stopped");
    }

    void PlatformCleanup() {
        for (auto& x : watchers_)
        {
            if (x->bQueued)
            {
                DWORD bytes;
                CancelIoEx (x->hDirectory, &x->overlapped);
                GetOverlappedResult (x->hDirectory, &x->overlapped, &bytes, TRUE);
            }
            CloseHandle(x->overlapped.hEvent);
            CloseHandle(x->hDirectory);
            delete x;
        }
    }

    std::list<WinWatcherData*> watchers_;
#elif __unix__
    void PlatformInit()
    {
        notify_fd_ = inotify_init1(IN_NONBLOCK);
        LC_FATAL(notify_fd_ == -1, "Could not init inotify! error: %d", errno);
    }

    void PlatformCleanup() 
    {
        for (auto &x: watch_files_) {
            inotify_rm_watch(notify_fd_, x.first);
        }
        watch_files_.clear();
        close(notify_fd_);
    }

    bool PlatformAddWatchDir(const char *path)
    {
        int fd = inotify_add_watch(notify_fd_, path, IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_MOVE);
        if (fd == -1) {
            L_ERROR("could not watch %s, code %d", path, errno);
        } else {
            watch_files_.emplace(fd, std::string{path});
            return true;
        }
        return false;
    }

    void HandleEvents()
    {
        alignas(alignof(struct inotify_event)) char buffer[4096] {};
        const struct inotify_event *event = nullptr;
        ssize_t len;

        for (;;)
        {
            len = read(notify_fd_, buffer, sizeof(buffer));
            if (len == EAGAIN)
                continue;
            else if (len <= 0)
                break;

            bool event_finished;
            int  path_idx = 0;
            std::string paths[2];
            FileSystemChangedReason reason;
            for (char *ptr = buffer; ptr < buffer + len; ptr = ptr + sizeof(struct inotify_event) + event->len)
            {
                event = (const struct inotify_event*)ptr;
                // L_INFO("wd: %d, mask: %08x, cookie: %08x, len: %u, name: %s", event->wd, event->mask, event->cookie, event->len, event->len ? event->name : "empty");
                auto it = watch_files_.find(event->wd);
                if (it == watch_files_.end())
                {
                    L_WARN("unknown dir for wd %d, ignored!", event->wd);
                    continue;
                }
                if (event->len == 0)
                {
                    L_WARN("not file, ignored!");
                    continue;
                }

                paths[path_idx++] = it->second + '/' + event->name;

                if (event->mask & IN_CREATE)
                {
                    reason = FileSystemChangedReason::eCreated;
                    event_finished = true;
                }
                else if (event->mask & IN_DELETE)
                {
                    reason = FileSystemChangedReason::eDeleted;
                    event_finished = true;
                }
                else if (event->mask & IN_CLOSE_WRITE)
                {
                    reason = FileSystemChangedReason::eModified;
                    event_finished = true;
                }
                else if (event->mask & IN_MOVED_FROM)
                {
                    reason = FileSystemChangedReason::eRenamed;
                }
                else if (event->mask & IN_MOVED_TO)
                {
                    event_finished = true;
                }

                if (event_finished) {
                    InvokeCallback(reason, paths[0], paths[1]);
                    event_finished = false;
                    path_idx = 0;
                }

                /*
                else
                {
                    std::string path = it->second;
                    if (event->len)
                        path = path + '/' + event->name;
                    if (event->mask & IN_CLOSE_WRITE) {
                        InvokeCallback(FileSystemChangedReason::eModified, path);
                    }
                    else if (event->mask & IN_DELETE) {
                        InvokeCallback(FileSystemChangedReason::eDeleted, path);
                    }
                    else if (event->mask & IN_CREATE) {
                        InvokeCallback(FileSystemChangedReason::eCreated, path);
                    }
                }
                */
            }
        }
    }

    void PlatformWatchProc()
    {
        L_INFO("Watch started");
        struct pollfd pfd{.fd = notify_fd_, .events = POLLIN, .revents = 0};
        while(watching_.load())
        {
            int nfd = poll(&pfd, 1, 1000); // poll 1 second
            if (nfd > 0 && pfd.events & POLLIN)
            {
                HandleEvents();
            }
        }
        L_INFO("Watch stopped");
    }

    int notify_fd_;
    std::map<int, std::string> watch_files_;
#endif
};


FileSystemWatcher:: FileSystemWatcher (): d{new FileSystemWatcherImpl}
{

}
FileSystemWatcher::~FileSystemWatcher ()
{
    delete d;
}
bool                FileSystemWatcher::AddWatchFolder (const char *path) const
{
    return d->AddWatchDir (path);
}
void                FileSystemWatcher::SetCallback (FileSystemChangedCallback callback) const
{
    d->SetCallback (callback);
}
void FileSystemWatcher::StartWatch () const
{
    d->StartWatch ();
}
void FileSystemWatcher::StopWatch () const
{
    d->StopWatch ();
}
